use std::fmt::{Debug, Write};
use std::ops;

use crate::geom::*;

pub const CHUNK_SIZE: usize = 16;
pub const CHUNK_SIZE_I32: i32 = CHUNK_SIZE as i32;
pub const CHUNK_NUM_BLOCKS: usize = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE;

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct BlkCoord(Vec3i);

impl BlkCoord {
    /// Creates a new block coord from components.
    pub fn new(x: i32, y: i32, z: i32) -> Self {
        Self(Vec3i::new(x, y, z))
    }

    /// Converts a chunk coord and subchunk position to a block coord.
    pub fn from_parts(cc: ChunkCoord, scp: SubchunkPos) -> Self {
        conv_chunk_poses_to_world(cc, scp)
    }

    /// Converts from a vec3f, rounding down to find the block coord.
    pub fn from_vec3f(v: Vec3f) -> Self {
        let x = v.x().floor() as i32;
        let y = v.y().floor() as i32;
        let z = v.z().floor() as i32;
        Self::new(x, y, z)
    }

    pub fn inner(&self) -> &Vec3i {
        &self.0
    }

    pub fn x(&self) -> i32 {
        self.0.x()
    }

    pub fn y(&self) -> i32 {
        self.0.y()
    }

    pub fn z(&self) -> i32 {
        self.0.z()
    }

    /// Gets the coord of the neighbor in a particular axis direction.
    pub fn neighbor_to(&self, dir: Direction) -> Self {
        Self(self.0 + dir.to_unit_vec())
    }

    /// Splits the block coord into a chunk coord and a subchunk position.
    pub fn split(&self) -> (ChunkCoord, SubchunkPos) {
        conv_world_pos_to_chunk(*self)
    }

    /// Returns the coord of the chunk this block would be in.
    pub fn chunk_coord(&self) -> ChunkCoord {
        let (cc, _) = self.split();
        cc
    }

    /// Returns the pos within the chunk this block would be.
    pub fn subchunk_pos(&self) -> SubchunkPos {
        let (_, scp) = self.split();
        scp
    }
}

impl From<Vec3i> for BlkCoord {
    fn from(value: Vec3i) -> Self {
        Self(value)
    }
}

impl Into<Vec3i> for BlkCoord {
    fn into(self) -> Vec3i {
        self.0
    }
}

impl Into<Vec3f> for BlkCoord {
    fn into(self) -> Vec3f {
        Vec3f::from(*self.inner())
    }
}

impl ops::Add<Vec3i> for BlkCoord {
    type Output = BlkCoord;
    fn add(self, rhs: Vec3i) -> Self::Output {
        Self(self.0 + rhs)
    }
}

impl ops::Sub<Vec3i> for BlkCoord {
    type Output = BlkCoord;
    fn sub(self, rhs: Vec3i) -> Self::Output {
        Self(self.0 - rhs)
    }
}

impl ops::Sub for BlkCoord {
    type Output = Vec3i;
    fn sub(self, rhs: BlkCoord) -> Self::Output {
        Vec3i::sub(self.0, rhs.0)
    }
}

pub const SUBCHUNK_COORD_MASK: u8 = 0x0f;
pub const SUBCHUNK_FULL_MASK: u16 = 0x0fff;

/// Returns a subchunk position, packed into a u16.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct SubchunkPos(u16);

impl SubchunkPos {
    /// New instance from coordinates.  All values must be <16.
    pub fn new(x: u8, y: u8, z: u8) -> Self {
        if x & !SUBCHUNK_COORD_MASK != 0 {
            panic!("x coord invalid");
        }

        if y & !SUBCHUNK_COORD_MASK != 0 {
            panic!("y coord invalid");
        }

        if z & !SUBCHUNK_COORD_MASK != 0 {
            panic!("z coord invalid");
        }

        Self((x as u16) << 8 | (y as u16) << 4 | z as u16)
    }

    /// Returns an iterator over all positions in a chunk.
    pub fn iter() -> impl Iterator<Item = SubchunkPos> {
        (0..CHUNK_NUM_BLOCKS).map(|i| SubchunkPos::from_raw(i as u16))
    }

    /// From an encoded u16.  Masks out top nibble of bits, if set.
    pub fn from_raw(v: u16) -> Self {
        Self(v & SUBCHUNK_FULL_MASK)
    }

    /// Returns the x coord.
    pub fn x(&self) -> u8 {
        ((self.0 >> 8) & SUBCHUNK_COORD_MASK as u16) as u8
    }

    /// Returns the y coord.
    pub fn y(&self) -> u8 {
        ((self.0 >> 4) & SUBCHUNK_COORD_MASK as u16) as u8
    }

    /// Returns the z coord.
    pub fn z(&self) -> u8 {
        (self.0 & SUBCHUNK_COORD_MASK as u16) as u8
    }

    /// Unpacks a subchunk pos into its constituent coords.
    pub fn unpack(&self) -> (u8, u8, u8) {
        (self.x(), self.y(), self.z())
    }

    /// Returns the raw u16.  Masks off the top nibble just in case.
    pub fn raw(&self) -> u16 {
        self.0 & SUBCHUNK_FULL_MASK
    }

    /// Returns true if this pos is on any of the faces of a chunk.
    pub fn is_at_face(&self) -> bool {
        self.x() == 0
            || self.x() == 15
            || self.y() == 0
            || self.y() == 15
            || self.z() == 0
            || self.z() == 15
    }

    /// Returns the position of a neighboring subchunk pos, if it stays within
    /// the bounds of the chunk.
    pub fn interior_neighbor_to(&self, dir: Direction) -> Option<SubchunkPos> {
        let dv = dir.to_unit_vec();

        // TODO this math seems too complicated
        let nx = match (self.x(), dv.x()) {
            (x, 0) => x,
            (0, -1) => return None,
            (15, 1) => return None,
            (x, dx) => (x as i32 + dx) as u8,
        };

        let ny = match (self.y(), dv.y()) {
            (y, 0) => y,
            (0, -1) => return None,
            (15, 1) => return None,
            (y, dy) => (y as i32 + dy) as u8,
        };

        let nz = match (self.z(), dv.z()) {
            (z, 0) => z,
            (0, -1) => return None,
            (15, 1) => return None,
            (z, dz) => (z as i32 + dz) as u8,
        };

        Some(SubchunkPos::new(nx, ny, nz))
    }

    /// Gets a relative subchunk pos, offset by some amount, returning the
    /// relative position and a chunk coordinate offset.
    pub fn offset_by(&self, off: Vec3i) -> (SubchunkPos, Vec3i) {
        let coord = BlkCoord::from_parts(ChunkCoord::from(Vec3i::zero()), *self) + off;
        let (cc, scp) = coord.split();
        (scp, cc.into())
    }

    /// Like `.offset_by`, but just to find a neighbor.
    pub fn global_neighbor_to(&self, dir: Direction) -> (SubchunkPos, Vec3i) {
        self.offset_by(dir.to_unit_vec())
    }
}

/// We use `{}` as the borders for printing a subchunk pos to distinguish it from a Vec3i pos.
impl Debug for SubchunkPos {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_char('{')?;
        f.write_fmt(format_args!("{},{},{}", self.x(), self.y(), self.z()))?;
        f.write_char('}')?;
        Ok(())
    }
}

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct ChunkCoord(Vec3i);

impl ChunkCoord {
    pub fn inner(&self) -> &Vec3i {
        &self.0
    }

    pub fn x(&self) -> i32 {
        self.0.x()
    }

    pub fn y(&self) -> i32 {
        self.0.y()
    }

    pub fn z(&self) -> i32 {
        self.0.z()
    }

    /// Gets the coord of the neighbor in a particular axis direction.
    pub fn neighbor_to(&self, dir: Direction) -> Self {
        Self(self.0 + dir.to_unit_vec())
    }
}

impl From<Vec3i> for ChunkCoord {
    fn from(value: Vec3i) -> Self {
        Self(value)
    }
}

impl Into<Vec3i> for ChunkCoord {
    fn into(self) -> Vec3i {
        self.0
    }
}

impl ops::Add<Vec3i> for ChunkCoord {
    type Output = ChunkCoord;
    fn add(self, rhs: Vec3i) -> Self::Output {
        Self(self.0 + rhs)
    }
}

impl ops::Sub<Vec3i> for ChunkCoord {
    type Output = ChunkCoord;
    fn sub(self, rhs: Vec3i) -> Self::Output {
        Self(self.0 - rhs)
    }
}

impl ops::Sub for ChunkCoord {
    type Output = Vec3i;
    fn sub(self, rhs: Self) -> Self::Output {
        Vec3i::sub(self.0, rhs.0)
    }
}

#[inline]
pub fn conv_world_pos_to_chunk(coord: BlkCoord) -> (ChunkCoord, SubchunkPos) {
    let m = CHUNK_SIZE_I32;

    let (x, y, z) = coord.inner().into();
    let (sx, sy, sz) = (x.rem_euclid(m), y.rem_euclid(m), z.rem_euclid(m));
    let (cx, cy, cz) = ((x - sx) / m, (y - sy) / m, (z - sz) / m);

    (
        ChunkCoord(Vec3i::new(cx, cy, cz)),
        SubchunkPos::new(sx as u8, sy as u8, sz as u8),
    )
}

#[inline]
pub fn conv_chunk_poses_to_world(cc: ChunkCoord, spos: SubchunkPos) -> BlkCoord {
    let (sx, sy, sz) = spos.unpack();
    let (cx, cy, cz) = cc.inner().into();
    Vec3i::new(
        cx * CHUNK_SIZE_I32 + sx as i32,
        cy * CHUNK_SIZE_I32 + sy as i32,
        cz * CHUNK_SIZE_I32 + sz as i32,
    )
    .into()
}
